package com.mobdev.notepad.shared.gdocs;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

import com.mobdev.notepad.shared.AbstractSyncActivity;
import com.mobdev.notepad.shared.AbstractSyncThread;
import com.mobdev.notepad.shared.DBConstants;
import com.mobdev.notepad.shared.NotepadStrings;
import com.mobdev.notepad.shared.gdocs.ConnectionManager.ResourceInfo;


public class SyncThread extends AbstractSyncThread
{

	private static final boolean DEBUG = false;



	  /**
     * The columns we are interested in from the database
     */
    private static final String[] PROJECTION = new String[] {
    	DBConstants._ID,
        DBConstants.REMOTE_ID,
        DBConstants.TITLE,
        DBConstants.CONTENTS,
        DBConstants.TAGS,
        DBConstants.MODIFIED_ON,
        DBConstants.SYNCED_ON,
        DBConstants.NEW_NOTE_CREATED_ON_PHONE,
        DBConstants.DELETED_FROM_PHONE,
        DBConstants.DONT_SYNC_THIS_NOTE,
    };

    protected static int IDX = 0;
    protected static final int COLUMN_INDEX_ID = IDX++;
    protected static final int COLUMN_INDEX_GDOCS_ID = IDX++;
    protected static final int COLUMN_INDEX_TITLE = IDX++;
    protected static final int COLUMN_INDEX_CONTENTS = IDX++;
    protected static final int COLUMN_INDEX_TAGS = IDX++;
    protected static final int COLUMN_INDEX_MODIFIED_ON = IDX++;
    protected static final int COLUMN_INDEX_SYNCED_ON = IDX++;
    protected static final int COLUMN_INDEX_NEW_NOTE_CREATED_ON_PHONE = IDX++;
    protected static final int COLUMN_INDEX_DELETED_FROM_PHONE = IDX++;
    protected static final int COLUMN_INDEX_DONT_SYNC_THIS_NOTE = IDX++;





	protected final AbstractSyncActivity activity;


	// Need handler for callbacks to the UI thread
	protected final Handler mHandler = new Handler();

	protected volatile boolean stop = false;

	protected final StringBuffer fatalError = new StringBuffer(500);
	protected final StringBuffer errorWhileSyncingTheseTitles = new StringBuffer(500);
	protected boolean someNotesCouldntBeDownloaded = false;

	protected String accountName = null;

	protected Runnable threadFinishedHandler = null;

	protected String directoryName = null;
	protected String directoryID = null;

	private ConnectionManager connectionManager;//connector to googleDocs

	public SyncThread(AbstractSyncActivity activity)
	{
		this.activity = activity;

		SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
		accountName =  preferences.getString("account_name", null);
		directoryName = preferences.getString("account_directory", null);

		if(directoryName != null)
			directoryName = directoryName.trim();

	}



	@Override
	public void run()
	{
		final AbstractSyncActivity activity = this.activity;

		setText("Logging in..   ", null);

		AccountManager accountManager = AccountManager.get(activity);
		Account[] accounts = accountManager.getAccountsByType("com.google");
		Account googleAccount = null;
        for(Account account : accounts) {
        	if(account.name.equals(accountName)) {
        		googleAccount = account;
        	}
        }

    	if(googleAccount == null) {
    		setText("Please select a google account.\n\nOpening settings..", null);
			try {         Thread.sleep(3000); } catch(Exception e) {}
			activity.showPreferences();
			activity.finish();
			return;
    	}


		int serverClockLag = 0;
		try
		{

			String authToken = null;
	        try {
	        	authToken = accountManager.blockingGetAuthToken(googleAccount, "writely", true);
	        	if(authToken == null) {
	        		//getAuthToken with callback to explicitly ask user for permission to access their account
	        		//based on http://www.finalconcept.com.au/article/view/android-account-manager-using-other-accounts
	        		accountManager.getAuthToken(googleAccount, "writely", true, new AccountManagerCallback<Bundle>() {
		        		public void run(AccountManagerFuture<Bundle> arg0) {
		        			try {
		        				Bundle result = arg0.getResult();
		        				if (result.containsKey(AccountManager.KEY_INTENT)) {
		        					Intent i = (Intent)result.get(AccountManager.KEY_INTENT);
		        					activity.startActivity(i);
		        				}
		        			} catch(Exception e) {
		        				Log.e(NotepadStrings.LOG_TAG, "Unexpected error: " + e, e);
		        			}
		        		}
		        	}, mHandler);
	        	}


				accountManager.invalidateAuthToken(googleAccount.type, authToken);  //always get a fresh auth token so you don't have to deal
				authToken = accountManager.blockingGetAuthToken(googleAccount, "writely", true);
	        } catch (Exception e) {
	        	String error = "Unable to login to account:\n" + googleAccount.name;
	        	Log.e(NotepadStrings.LOG_TAG, error, e);
				fatalError.append(error);
				throw new IOException(error);
	        }

			if(authToken == null)
			{
				String error = "Could not login.\nPlease check the notification bar for a request by this app to access:\n" + googleAccount.name;
				fatalError.append(error);
				throw new IOException(error);
			}



			if(stop)
			{
				fatalError.append("canceled");
				throw new IOException("Stop requested");
			}




			//get directory ID and lead user to creating new directory if it doesn't exist, or selecting existing directory, etc.
	        connectionManager = new ConnectionManager();
			directoryID = connectionManager.getFolderID(authToken, directoryName);
			if(directoryID == null)
			{
				//directory does not exist, try to create it
				setText("Creating folder:\n" + directoryName, null);
				directoryID = connectionManager.createNewFolder(authToken, directoryName);

			}

			/* UPLOAD ALL NEW NOTES TO THE SERVER */
			uploadNewNotesToServerGoogle(authToken);

			/* SYNC EXISTING NOTES */
			HashSet<String> gdocsIdsOfNotesThatExistOnServer = new LinkedHashSet<String>();
			compareExistingNotesGoogle(authToken, gdocsIdsOfNotesThatExistOnServer, serverClockLag);

			/* DELETE NOTES THAT HAVE BEEN DELETED ON THE SERVER */
			deleteNotesDeletedOnTheServerGoogle(authToken, gdocsIdsOfNotesThatExistOnServer, serverClockLag);
		}
		catch(IOException e)
		{
			Log.e(NotepadStrings.LOG_TAG, "IOException: " + e, e);
		}
		catch(Exception e)
		{
			if(fatalError.length() > 0)
				fatalError.delete(0, fatalError.length() - 1);
			fatalError.append("Unexpected error occured");
			Log.e(NotepadStrings.LOG_TAG, "Unexpected error: " + e, e);
		}
		finally
		{
			if(threadFinishedHandler != null)
        	{
        		threadFinishedHandler.run();
        	}

        	if(fatalError.length() > 0)
        	{
		    	setText("Error:  " + fatalError.toString(), null);
        	}
        	else if(errorWhileSyncingTheseTitles.length() > 0 || someNotesCouldntBeDownloaded)
        	{
				if(errorWhileSyncingTheseTitles.length() > 0)
        			setText("Error - couldn't sync:\n", errorWhileSyncingTheseTitles.toString());
        		if(someNotesCouldntBeDownloaded)
        			setText("Error: some notes failed to download", null);
        	}
        	else
        	{
        		setText("Success! All notes synced.", null);
        	}

        	try { Thread.sleep(1800); } catch(Exception e) {}
			activity.finish();
		}
	}

	private void uploadNewNotesToServerGoogle(String authToken) throws IOException
	{
		// Get URI of notes database
		//final ContentResolver contentResolver = activity.getContentResolver();
		final Uri tableUri = activity.getIntent().getData();

		// Perform a managed query. The Activity will handle closing and requerying the cursor when needed.
		// Find all notes that have been created on the phone
        Cursor newItemsQuery = activity.managedQuery(tableUri, PROJECTION,
        		"("+DBConstants.NEW_NOTE_CREATED_ON_PHONE+"=1)", // AND " + "("+DBConstants.DELETED_FROM_PHONE+"=0)",
        		null, DBConstants.DEFAULT_SORT_ORDER);

        //CursorLoader cursorLoader = new CursorLoader(activity, tableUri, PROJECTION, "("+DBConstants.NEW_NOTE_CREATED_ON_PHONE+"=1)", null, DBConstants.DEFAULT_SORT_ORDER);
        //Cursor newItemsQuery = cursorLoader.loadInBackground();
        try
        {
	        newItemsQuery.moveToFirst();
	        while(!newItemsQuery.isAfterLast())
	        {
	        	//get note details from db
	        	final int _id = newItemsQuery.getInt( COLUMN_INDEX_ID );
	        	final boolean is_deleted_from_phone = ( newItemsQuery.getInt( COLUMN_INDEX_DELETED_FROM_PHONE ) != 0 );
	        	final Uri noteUri = ContentUris.withAppendedId(tableUri, _id);

	        	if(is_deleted_from_phone)
	        	{
	        		//this note was created on the phone and was deleted before being synced even once
	        		if(DEBUG)
		        		Log.i(NotepadStrings.LOG_TAG, "NEW NOTE ON PHONE - marked for deletion. Deleting db record ["+ _id + "] without uploading.");

	        		deleteNoteFromPhone(noteUri);
	        	}
	        	else
	        	{
	        		//this note was created on the phone and needs to be uploaded for the 1st time

	        		//upload the note to the server
		        	final String title = newItemsQuery.getString( COLUMN_INDEX_TITLE );
		        	final String contents = newItemsQuery.getString( COLUMN_INDEX_CONTENTS );
		        	final String tagsSeparatedWithSpaces = newItemsQuery.getString( COLUMN_INDEX_TAGS );

		        	boolean created = createNoteOnServerGoogle(authToken, _id, title, contents, tagsSeparatedWithSpaces);
		        	if(!created)
		        	{
		        		newItemsQuery.moveToNext();
		        		continue;
		        	}
				}

	        	if(stop)
				{
					fatalError.append("canceled");
					throw new IOException("Stop requested");
				}

	        	newItemsQuery.moveToNext();
	        }
        }
        finally
        {
	        newItemsQuery.close();
	        newItemsQuery = null;
        }
	}

	private boolean createNoteOnServerGoogle(String authToken, int _id, String title, String contents, String tagsSeparatedWithSpaces) throws IOException
	{
		final ContentResolver contentResolver = activity.getContentResolver();
		final Uri tableUri = activity.getIntent().getData();
		final Uri noteUri = ContentUris.withAppendedId(tableUri, _id);

        setText("Uploading:\n", title.replace('\n', ' '));

    	if(DEBUG)
    		Log.i(NotepadStrings.LOG_TAG, "UPLOADING NEW NOTE: [" + _id + "]: " + title);

    	boolean saved = false;
    	String googleID = "";
    	try
    	{

    		//saved = connectionManager.uploadNewDocumentToFolder(authToken, "google notepad", title, contents, docIdentifiers);
    		googleID = connectionManager.uploadNewDocumentToFolderByID(authToken, directoryID, title, contents);
    		//please note that the returned ID includes full http path with directory ID included; we need to extract the documentID part from it
    		googleID = googleID.split("%3A")[2];
    		saved = true;
    	}
    	catch(Exception e)
    	{
    		Log.e(NotepadStrings.LOG_TAG, "Could not upload new document " + title + " to server: " + e, e);
    	}

    	if(!saved || googleID == null || googleID.length() == 0)
    	{
    		errorWhileSyncingTheseTitles.append(title + "\n");
    		return false;
    	}

    	//update table - mark note as not new, update gdocs id, sync time
    	int now = (int) (System.currentTimeMillis() / 1000);

        ContentValues values = new ContentValues();
        values.put(DBConstants.NEW_NOTE_CREATED_ON_PHONE, 0);
        values.put(DBConstants.REMOTE_ID, googleID);
        values.put(DBConstants.SYNCED_ON, now);

        int updated = contentResolver.update(noteUri, values, null, null);
    	if(updated <= 0)
    	{
    		fatalError.append("Couldn't update database" + "\n");
    		throw new IOException("Couldn't update note: " + noteUri);
    	}

    	return true;
	}

	private void compareExistingNotesGoogle(String authToken, HashSet<String> gdocsIdsOfNotesThatExistOnServer, int serverClockLag) throws Exception
	{
		final ContentResolver contentResolver = activity.getContentResolver();
		final Uri tableUri = activity.getIntent().getData();

		//boolean conflictTagged = false; //tag only one conflict per sync

		/* DOWNLOAD NOTES FROM THE SERVER */
		setText("Getting data..   ", null);


		//download
		ArrayList<ResourceInfo> allDocuments = connectionManager.getAllDocumentsInFolderByID(authToken, directoryID);

		if(stop)
		{
			fatalError.append("canceled");
			throw new IOException("Stop requested");
		}

		//go through each note
		if(allDocuments.isEmpty())
		{
			Log.i(NotepadStrings.LOG_TAG, "No documents found on server");
			return;
		}

		//if(DEBUG)
		//	Log.i(NotePad.LOG_TAG, "Documents: " + documentsResponse);

		//String[] documents = documentsResponse.split("</document>");
		for(ResourceInfo ri : allDocuments)
		{

	    	//parse values from server response
			ServerSideNoteInfo serverSideNoteInfo = new ServerSideNoteInfo();

			try
			{
				//please note that document id, which is in documentDetails[0], includes full http path as well as directory id, we have to extract documentID part from it, thus the use of split method
				serverSideNoteInfo.server_gdocs_id = ri.id.split("%3A")[2];
				serverSideNoteInfo.server_title = ri.title;

				//download text for the given document
				serverSideNoteInfo.server_contents = connectionManager.getDocumentTextByID(authToken, serverSideNoteInfo.server_gdocs_id);
				serverSideNoteInfo.server_tags = "";  //no tags for now
				serverSideNoteInfo.server_created_on = (int)(ri.publishedDate.getTime()/1000);
				serverSideNoteInfo.server_modified_on = (int)(ri.updatedDate.getTime()/1000);

				if(DEBUG)
					Log.i(NotepadStrings.LOG_TAG, "Got note: (" + serverSideNoteInfo.server_gdocs_id + ") - server title:" + serverSideNoteInfo.server_title + " contents: \"" + serverSideNoteInfo.server_contents.replace("\n","\\n").replace("\r", "\\r") + "\", modified on: " + serverSideNoteInfo.server_modified_on);

			}
			catch(Exception e)
			{
				someNotesCouldntBeDownloaded |= true;
	    		Log.e(NotepadStrings.LOG_TAG, "Couldn't get details for resource: " + ri + "\n", e);
	    		continue;
			}

			gdocsIdsOfNotesThatExistOnServer.add(serverSideNoteInfo.server_gdocs_id);

			//Query notes db for this document
			Cursor noteQuery = activity.managedQuery(tableUri, PROJECTION, DBConstants.REMOTE_ID+"=?", new String[] { serverSideNoteInfo.server_gdocs_id }, DBConstants.DEFAULT_SORT_ORDER);
			//CursorLoader cursorLoader = new CursorLoader(activity, tableUri, PROJECTION, DBConstants.HELIPAD_ID+"=?", new String[] { serverSideNoteInfo.server_gdocs_id }, DBConstants.DEFAULT_SORT_ORDER);
	        //Cursor noteQuery = cursorLoader.loadInBackground();
			try
			{
		        if(noteQuery.getCount() == 0)
		        {
		        	//NOTE WAS CREATED ON THE SERVER

		        	if(DEBUG)
		        		Log.i(NotepadStrings.LOG_TAG, "DOWNLOADING NEW NOTE: (" + serverSideNoteInfo.server_gdocs_id + "): - mod_time:" + serverSideNoteInfo.server_modified_on + ", " + new Date((long)serverSideNoteInfo.server_modified_on * 1000) + ", server title:" + serverSideNoteInfo.server_title);


		        	//insert note into db
		        	int now = (int) (System.currentTimeMillis() / 1000);
		        	ContentValues values = new ContentValues();
		            values.put(DBConstants.REMOTE_ID, serverSideNoteInfo.server_gdocs_id);
		            values.put(DBConstants.TITLE, serverSideNoteInfo.server_title);
		            values.put(DBConstants.CONTENTS, serverSideNoteInfo.server_contents);
		            values.put(DBConstants.TAGS, serverSideNoteInfo.server_tags);
		            values.put(DBConstants.CREATED_ON, serverSideNoteInfo.server_created_on);
		            values.put(DBConstants.MODIFIED_ON, serverSideNoteInfo.server_modified_on );
		            values.put(DBConstants.SYNCED_ON, Math.max(now, serverSideNoteInfo.server_modified_on) ); //handle server clock being ahead of phone clock
		            values.put(DBConstants.NEW_NOTE_CREATED_ON_PHONE, 0);
		            values.put(DBConstants.DELETED_FROM_PHONE, 0);
		            values.put(DBConstants.DONT_SYNC_THIS_NOTE, 0);

		            final Uri noteUri = contentResolver.insert(tableUri, values);
					if (noteUri == null)
					{
						//handle couldn't insert note error
						fatalError.append("Couldn't update database" + "\n");
						throw new IOException("Couldn't insert note into db: " + tableUri);
					}
		        }
		        else
		        {
		        	//NOTE HAS BEEN SYNCED WITH PHONE AT LEAST ONCE BEFORE

		        	noteQuery.moveToFirst();

		        	LocalNoteInfo localNoteInfo = new LocalNoteInfo();
		        	localNoteInfo._id = noteQuery.getInt( COLUMN_INDEX_ID );
        			localNoteInfo.local_title = noteQuery.getString( COLUMN_INDEX_TITLE );
        			localNoteInfo.local_contents = noteQuery.getString( COLUMN_INDEX_CONTENTS );
        			localNoteInfo.local_tags = noteQuery.getString( COLUMN_INDEX_TAGS );
		        	localNoteInfo.is_deleted_from_phone = ( noteQuery.getInt( COLUMN_INDEX_DELETED_FROM_PHONE ) != 0 );
		        	localNoteInfo.dont_sync_this_note = noteQuery.getInt( COLUMN_INDEX_DONT_SYNC_THIS_NOTE );
		        	localNoteInfo.local_modified_on = noteQuery.getInt( COLUMN_INDEX_MODIFIED_ON );
		        	localNoteInfo.synced_on = noteQuery.getInt( COLUMN_INDEX_SYNCED_ON );


		        	if(DEBUG) {
		        		Log.i(NotepadStrings.LOG_TAG, "About to sync: (" + serverSideNoteInfo.server_gdocs_id + "), " +
		        			"\nlocal:  " + localNoteInfo.local_modified_on + " - " + new Date((long) localNoteInfo.local_modified_on * 1000) +  ", " +
		        			"\nsynced: " + localNoteInfo.synced_on + " - " + new Date((long) localNoteInfo.synced_on * 1000) +
		        			"\nserver: " + serverSideNoteInfo.server_modified_on + " - " + new Date((long)serverSideNoteInfo.server_modified_on * 1000) + ", ");

		        	}

		        	//skip notes that have been marked as "dont_sync_this_note" when the user clicked the 'skip' button in the sync conflict resolution prompt, and the note hasn't changed since then
		    		if((localNoteInfo.dont_sync_this_note > 0) && (localNoteInfo.local_modified_on <= localNoteInfo.dont_sync_this_note) && (serverSideNoteInfo.server_modified_on <= localNoteInfo.dont_sync_this_note))
		    		{
		    			continue;
		    		}


		        	if( (localNoteInfo.local_modified_on <= localNoteInfo.synced_on) && (serverSideNoteInfo.server_modified_on <= localNoteInfo.synced_on) )
		        	{
		        		//NOTE IS CURRENTLY IN SYNC

		        		if(localNoteInfo.is_deleted_from_phone)
		        		{
			        		if(DEBUG)
		    	        		Log.i(NotepadStrings.LOG_TAG, "NOTE MARKED FOR DELETION ON PHONE: (" + serverSideNoteInfo.server_gdocs_id + ") " + serverSideNoteInfo.server_title);


			        		//TODO give user way to disable this prompt in settings
		        			//note is reported to be in sync with the server, but is deleted on phone and exists on the server
			        		final String text = "The note:\n" +
		    				"\"" + serverSideNoteInfo.server_title + "\"\n" +
		    				"was deleted from phone.\n" +
		    				"Sync options:\n";
		        			handleConflict(authToken, text, localNoteInfo, serverSideNoteInfo, serverClockLag,
		        					ConflictResolutionOptions.DELETE_FROM_SERVER,
		        					ConflictResolutionOptions.DOWNLOAD_TO_PHONE,
			        				ConflictResolutionOptions.SKIP);
			        	}
		        		else
		        		{
		        			//nothing needs to be done
		        			if(DEBUG)
			        			Log.i(NotepadStrings.LOG_TAG, "NOTE ALREADY IN SYNC: (" + serverSideNoteInfo.server_gdocs_id + ") " + serverSideNoteInfo.server_title);

		        		}
		        	}
		        	else if( (localNoteInfo.local_modified_on > localNoteInfo.synced_on) && (serverSideNoteInfo.server_modified_on <= localNoteInfo.synced_on) )
		        	{
		        		//NOTE NEEDS TO BE UPLOADED
		        		if(localNoteInfo.is_deleted_from_phone)
		        		{
		        			//prompt user to confirm delete from server - this isn't really a conflict, its just to make sure the user knows about and authorizes the deletion
		        			final String text = "The note:\n" +
		    				"\"" + serverSideNoteInfo.server_title + "\"\n" +
		    				"was deleted from phone.\n" +
		    				"Sync options:\n";
		        			handleConflict(authToken, text, localNoteInfo, serverSideNoteInfo, serverClockLag,
		        					ConflictResolutionOptions.DELETE_FROM_SERVER,
		        					ConflictResolutionOptions.DOWNLOAD_TO_PHONE,
			        				ConflictResolutionOptions.SKIP);
		        			/*
			        		boolean deleted = deleteNoteFromServer(localNoteInfo, serverSideNoteInfo);
			        		if(!deleted)
			        		{
			        			continue;
			        		}
			        		*/
			        	}
		        		else
		        		{
			        		//boolean uploaded = uploadNoteToServer(localNoteInfo, serverSideNoteInfo, serverClockLag);
		        			boolean uploaded = uploadNoteToServerGoogle(authToken, localNoteInfo, serverSideNoteInfo, serverClockLag);
			        		if(!uploaded)
			        		{
			        			continue;
			        		}
		        		}
		        	}
		        	else if( (localNoteInfo.local_modified_on <= localNoteInfo.synced_on) && (serverSideNoteInfo.server_modified_on > localNoteInfo.synced_on) )
		        	{
		        		//NOTE NEEDS TO BE DOWNLOADED
		        		if(DEBUG)
		        			Log.i(NotepadStrings.LOG_TAG, "DOWNLOADING NOTE (" + serverSideNoteInfo.server_gdocs_id + ") : " + serverSideNoteInfo.server_title);

		        		if(localNoteInfo.is_deleted_from_phone)
		        		{
		        			//note is marked as deleted on phone, but has been synced, and then modified on the server.
		        			final String text = "The note:\n" +
		    				"\"" + serverSideNoteInfo.server_title + "\"\n" +
		    				"was deleted from phone.\n" +
		    				"Sync options:\n";
		        			handleConflict(authToken, text, localNoteInfo, serverSideNoteInfo, serverClockLag,
		        					ConflictResolutionOptions.DELETE_FROM_SERVER,
		        					ConflictResolutionOptions.DOWNLOAD_TO_PHONE,
			        				ConflictResolutionOptions.SKIP);
		        		}
		        		else
		        		{
		        			//downloadNoteToPhone(localNoteInfo, serverSideNoteInfo);
							downloadNoteToPhoneGoogle(authToken, localNoteInfo, serverSideNoteInfo);
		        		}
		        	}
		        	else if( (localNoteInfo.local_modified_on > localNoteInfo.synced_on) && (serverSideNoteInfo.server_modified_on > localNoteInfo.synced_on) )
		        	{
		        		//CONFLICT
		        		if(DEBUG)
		        			Log.i(NotepadStrings.LOG_TAG, "CONFLICT ON (" + serverSideNoteInfo.server_gdocs_id + ") : " + serverSideNoteInfo.server_title);

		        		if(localNoteInfo.is_deleted_from_phone)
		        		{
		        			//note was marked as deleted on the phone and modified on the server after being synced
		        			final String text = "CONFLICT:\n" +
		    				"\"" + serverSideNoteInfo.server_title + "\"\n" +
		    				"edited on server,\n" +
		    				"deleted from phone.\n" +
		    				"\n" +
		    				"To resolve:";
		        			handleConflict(authToken, text, localNoteInfo, serverSideNoteInfo, serverClockLag,
			        				ConflictResolutionOptions.DOWNLOAD_TO_PHONE,
			        				ConflictResolutionOptions.DELETE_FROM_SERVER,
			        				ConflictResolutionOptions.SKIP);
		        		}
		        		else
		        		{
		        			//note was modified on the phone and on the server after being synced
		        			final String text = "CONFLICT:\n" +
		    				"\"" + serverSideNoteInfo.server_title + "\"\n" +
		    				"edited on server\n" +
		    				"and on phone.\n" +
		    				"\n" +
		    				"To resolve:";
		        			handleConflict(authToken, text, localNoteInfo, serverSideNoteInfo, serverClockLag,
			        				ConflictResolutionOptions.DOWNLOAD_TO_PHONE,
			        				ConflictResolutionOptions.UPLOAD_TO_SERVER,
			        				ConflictResolutionOptions.SKIP);
		        		}

		        		//conflictOnTheseTitles.append(serverSideNoteInfo.server_title);
		        	}
		        	else
		        	{
		        		throw new IllegalStateException("Logic failed");
		        	}
		        } // end-if  note has been synced at least once before
			}
			finally
			{
				noteQuery.close();
				noteQuery = null;
			}
		} // for-loop over documents from server
	}

	private void deleteNotesDeletedOnTheServerGoogle(String authToken, HashSet<String> gdocsIdsOfNotesThatExistOnServer, int serverClockLag) throws Exception
	{

		// Get URI of notes database
		//final ContentResolver contentResolver = activity.getContentResolver();
		final Uri tableUri = activity.getIntent().getData();

		// Perform a managed query. The Activity will handle closing and requerying the cursor when needed.
		// Find all notes that have been created on the phone
        Cursor allNotesQuery = activity.managedQuery(tableUri, PROJECTION, null, null, DBConstants.DEFAULT_SORT_ORDER);
        //CursorLoader cursorLoader = new CursorLoader(activity, tableUri, PROJECTION, null, null, DBConstants.DEFAULT_SORT_ORDER);
        //Cursor allNotesQuery = cursorLoader.loadInBackground();
        try
        {
        	allNotesQuery.moveToFirst();
        	while(!allNotesQuery.isAfterLast())
	        {
        		//check whether this note exists on the server
        		String googleID = allNotesQuery.getString( COLUMN_INDEX_GDOCS_ID );

        		//if( gdocsIdsOfNotesThatExistOnServer.contains(gdocsId) )
        		if(gdocsIdsOfNotesThatExistOnServer.contains(googleID))
        		{
        			allNotesQuery.moveToNext();
        			continue; //this note still exists on the server
        		}

        		//get note details from db
        		LocalNoteInfo localNoteInfo = new LocalNoteInfo();
	        	localNoteInfo._id = allNotesQuery.getInt( COLUMN_INDEX_ID );
	        	localNoteInfo.local_title = allNotesQuery.getString( COLUMN_INDEX_TITLE );
	        	localNoteInfo.local_contents = allNotesQuery.getString( COLUMN_INDEX_CONTENTS );
    			localNoteInfo.local_tags = allNotesQuery.getString( COLUMN_INDEX_TAGS );
	        	localNoteInfo.is_deleted_from_phone = ( allNotesQuery.getInt( COLUMN_INDEX_DELETED_FROM_PHONE ) != 0 );
	        	localNoteInfo.dont_sync_this_note = allNotesQuery.getInt( COLUMN_INDEX_DONT_SYNC_THIS_NOTE );
	        	localNoteInfo.local_modified_on = allNotesQuery.getInt( COLUMN_INDEX_MODIFIED_ON );
	        	localNoteInfo.synced_on = allNotesQuery.getInt( COLUMN_INDEX_SYNCED_ON );

	        	final Uri noteUri = ContentUris.withAppendedId(tableUri, localNoteInfo._id);

	        	//skip notes that have been marked as "dont_sync_this_note" when the user clicked the 'skip' button in the sync conflict resolution prompt
	    		if((localNoteInfo.dont_sync_this_note > 0) && (localNoteInfo.local_modified_on <= localNoteInfo.dont_sync_this_note))
	    		{
	    			//clean up any notes marked for deletetion on the phone that have been deleted on the server
	    			if(localNoteInfo.is_deleted_from_phone)
		        	{
		        		//this note was deleted from the server and from the phone
		        		if(DEBUG)
			        		Log.i(NotepadStrings.LOG_TAG, "NEW NOTE ON PHONE - marked for deletion. Deleting db record ["+ localNoteInfo._id + "] without uploading.");

		        		//completely remove the record
		        		deleteNoteFromPhone(noteUri);

		        	}

	    			allNotesQuery.moveToNext();

	    			continue;
	    		}



	        	if(localNoteInfo.local_modified_on <= localNoteInfo.synced_on)
	        	{
	        		/* DELETE THE NOTE FROM THE PHONE */
	        		//this note has been deleted on the server. Delete note record from db.

	        		//TODO give user way to disable this prompt in settings
	        		//prompt user to confirm delete from server - this isn't really a conflict, its just to make sure the user knows about and authorizes the deletion
        			final String text = "The note:\n" +
    				"\"" + localNoteInfo.local_title + "\"\n" +
    				"was deleted on server.\n" +
    				"Sync options:\n";
        			handleConflict(authToken, text, localNoteInfo, null, serverClockLag,
	        				ConflictResolutionOptions.DELETE_FROM_PHONE,
	        				ConflictResolutionOptions.CREATE_ON_SERVER,
	        				ConflictResolutionOptions.SKIP );

	        		//deleteNoteFromPhone(noteUri);
	        	}
	        	else if(localNoteInfo.local_modified_on > localNoteInfo.synced_on)
	        	{

		        	if(localNoteInfo.is_deleted_from_phone)
		        	{
		        		//this note was deleted from the server and from the phone at the same time
		        		if(DEBUG)
			        		Log.i(NotepadStrings.LOG_TAG, "NEW NOTE ON PHONE - marked for deletion. Deleting db record ["+ localNoteInfo._id + "] without uploading.");

		        		deleteNoteFromPhone(noteUri);

		        	}
		        	else
		        	{
		        		//this note was edited on the phone and deleted from the server before being synced
		        		//handle conflict
		        		final String text = "CONFLICT:\n" +
	    				"\"" + localNoteInfo.local_title + "\"\n" +
	    				"deleted on server,\n" +
	    				"edited on phone.\n" +
	    				"\n" +
	    				"To resolve:";
		        		handleConflict(authToken, text, localNoteInfo, null, serverClockLag,
		        				ConflictResolutionOptions.DELETE_FROM_PHONE,
		        				ConflictResolutionOptions.CREATE_ON_SERVER,
		        				ConflictResolutionOptions.SKIP);
		        	}
	        	}

	        	if(stop)
				{
					fatalError.append("canceled");
					throw new IOException("Stop requested");
				}

	        	allNotesQuery.moveToNext();
	        }
        }
        finally
        {
	        allNotesQuery.close();
	        allNotesQuery = null;
        }
	}


	/**
	 * Resolves conflict
	 * @param text The text to show the user
	 * @param localNoteInfo
	 * @param serverSideNoteInfo  Can be null
	 * @param serverClockLag
	 * @param option0
	 * @param option1
	 * @param option2 Can be null
	 * @throws IOException
	 */
	private void handleConflict(String authToken, String text, LocalNoteInfo localNoteInfo, ServerSideNoteInfo serverSideNoteInfo, int serverClockLag,
			ConflictResolutionOptions option0,
			ConflictResolutionOptions option1,
			ConflictResolutionOptions option2) throws IOException, Exception
	{


		setText(text, null);
		int result;
		if(option2 == null)
		{
			result = askUser(new String[] { option0.toString(), option1.toString(), null });
		}
		else
		{
			result = askUser(new String[] { option0.toString(), option1.toString(), option2.toString() });
		}

		ConflictResolutionOptions choice;
		switch(result)
		{
		case 0: choice = option0; break;
		case 1: choice = option1; break;
		case 2: choice = option2; break;
		default: throw new IllegalStateException("Unexpected choice: " + result);
		}

		final Uri tableUri = activity.getIntent().getData();
		final Uri noteUri = ContentUris.withAppendedId(tableUri, localNoteInfo._id);

		switch(choice) {
		case DELETE_FROM_PHONE:
			deleteNoteFromPhone(noteUri);
			break;
		case DELETE_FROM_SERVER:
			//deleteNoteFromServer(localNoteInfo, serverSideNoteInfo);
			deleteNoteFromServerGoogle(authToken, localNoteInfo, serverSideNoteInfo);//ok
			break;
		case DOWNLOAD_TO_PHONE:
			//downloadNoteToPhone(localNoteInfo, serverSideNoteInfo);
			downloadNoteToPhoneGoogle(authToken, localNoteInfo, serverSideNoteInfo);//ok
			break;
		case UPLOAD_TO_SERVER:
			//uploadNoteToServer(localNoteInfo, serverSideNoteInfo, serverClockLag);
			uploadNoteToServerGoogle(authToken, localNoteInfo, serverSideNoteInfo, serverClockLag);//ok
			break;
		case CREATE_ON_SERVER:
			//createNoteOnServer(localNoteInfo._id, localNoteInfo.local_title, localNoteInfo.local_contents, localNoteInfo.local_tags);
			createNoteOnServerGoogle(authToken, localNoteInfo._id, localNoteInfo.local_title, localNoteInfo.local_contents, localNoteInfo.local_tags);
			break;
		case SKIP:

			/* If the note was deleted from either the server or the phone, don't attempt to sync the note in the future.
			 * Otherwise, if the note still exists on both, continue prompting the user to resolve the conflict on subsequent syncs.
			 */
			markAsDontSyncThisNote(noteUri, serverSideNoteInfo); //always mark as skip - since skip is now handled more normally
			break;
		default:
			throw new IllegalStateException("Unexpected enum value: " + choice);
		}
	}


	/**
	 * @param noteUri
	 * @param serverSideNoteInfo
	 * @throws IOException
	 */
	private void markAsDontSyncThisNote(final Uri noteUri, ServerSideNoteInfo serverSideNoteInfo) throws IOException {
		//mark the note for skipping the sync.
		final ContentResolver contentResolver = activity.getContentResolver();

		int now = (int) (System.currentTimeMillis() / 1000);
		ContentValues values = new ContentValues();
		if(serverSideNoteInfo == null)
		{
			values.put(DBConstants.DONT_SYNC_THIS_NOTE, now);
		}
		else
		{
			values.put(DBConstants.DONT_SYNC_THIS_NOTE, Math.max(now, serverSideNoteInfo.server_modified_on));
		}
		/* DON'T update 'SYNCED_ON'. This way, the conflict state is maintained, so that, if the note is edited (lets say on the phone),
		 * the note will remain in a "conflict" state, and not switch into a normal "updated" state at which point it would be synced
		 * without a prompt, and the conflicting changes would be lost.
		 *
		if(serverSideNoteInfo == null)
		{
			values.put(DBConstants.SYNCED_ON, now);
		}
		else
		{
			values.put(DBConstants.SYNCED_ON, Math.max(now, serverSideNoteInfo.server_modified_on));
		}
		*/
		int updated = contentResolver.update(noteUri, values, null, null);
		if(updated <= 0) {
			fatalError.append("Couldn't update database" + "\n");
			throw new IOException("Couldn't update note: " + noteUri);
		}
	}






	/**
	 * @param localNoteInfo TODO
	 * @throws IOException
	 */
	private void deleteNoteFromPhone(Uri noteUri) throws IOException {

		final ContentResolver contentResolver = activity.getContentResolver();


		int updated = contentResolver.delete(noteUri, null, null);
		if(updated <= 0) {
			fatalError.append("Couldn't update database" + "\n");
			throw new IOException("Couldn't update note: " + noteUri);
		}
	}

	private boolean deleteNoteFromServerGoogle(String authToken, LocalNoteInfo localNoteInfo, ServerSideNoteInfo serverSideNoteInfo) throws Exception
	{

		//final ContentResolver contentResolver = activity.getContentResolver();
		final Uri tableUri = activity.getIntent().getData();
		final Uri noteUri = ContentUris.withAppendedId(tableUri, localNoteInfo._id);

    	setText("Deleting from server:\n", serverSideNoteInfo.server_title);

    	try {
    		connectionManager.deleteDocumentByID(authToken, serverSideNoteInfo.server_gdocs_id);
    	} catch(Exception e) {
    		if(DEBUG)
    			Log.e(NotepadStrings.LOG_TAG, "deleteDocumentByID(..) call failed for note '" + localNoteInfo.local_title + "'\nNote contents: \n" + localNoteInfo.local_contents, e);
    		else
    			Log.e(NotepadStrings.LOG_TAG, "deleteDocumentByID(..) call failed for note '" + localNoteInfo.local_title + "'", e);

    		errorWhileSyncingTheseTitles.append(serverSideNoteInfo.server_title.replace('\n', ' ') + "\n");

    		return false;
    	}

		deleteNoteFromPhone(noteUri);

		return true;
	}

	private boolean uploadNoteToServerGoogle(String authToken, LocalNoteInfo localNoteInfo, ServerSideNoteInfo serverSideNoteInfo, int serverClockLag) throws Exception
	{
		final ContentResolver contentResolver = activity.getContentResolver();
		final Uri tableUri = activity.getIntent().getData();
		final Uri noteUri = ContentUris.withAppendedId(tableUri, localNoteInfo._id);

		// Get URI of notes database
		if(DEBUG)
		{
			Log.i(NotepadStrings.LOG_TAG, "ULOADING_NOTE: (" + serverSideNoteInfo.server_gdocs_id + ") " + serverSideNoteInfo.server_title);
		}

		setText("Uploading:\n", localNoteInfo.local_title.replace('\n', ' '));

    	try  {
    		connectionManager.updateDocumentContentByID(authToken, localNoteInfo.local_title.replace('\n', ' '), serverSideNoteInfo.server_gdocs_id, localNoteInfo.local_contents);
    	} catch(Exception e) {
    		if(DEBUG)
    			Log.e(NotepadStrings.LOG_TAG, "updateDocumentContentByID(..) call failed for note '" + localNoteInfo.local_title + "'\nNote contents: \n" + localNoteInfo.local_contents, e);
    		else
    			Log.e(NotepadStrings.LOG_TAG, "updateDocumentContentByID(..) call failed for note '" + localNoteInfo.local_title + "'", e);

    		errorWhileSyncingTheseTitles.append(localNoteInfo.local_title + "\n");

    		return false;
    	}



    	//update sync time
    	int now = (int) (System.currentTimeMillis() / 1000);
        ContentValues values = new ContentValues();

        //if server clock is ahead of local clock, set the SYNCED_ON time ahead
        //(DOWNSIDE: won't upload until this difference elapses, even if changes made on phone version)
        int serverClock = now - serverClockLag;
        values.put(DBConstants.SYNCED_ON, Math.max(now, serverClock + 3) );  //add 3 seconds to be sure
        values.put(DBConstants.DONT_SYNC_THIS_NOTE, 0);
		values.put(DBConstants.DELETED_FROM_PHONE, 0);

        int updated = contentResolver.update(noteUri, values, null, null);
        if(updated <= 0) {
        	fatalError.append("Couldn't update database" + "\n");
    		throw new IOException("Couldn't update note: " + noteUri);
    	}

        return true;
	}

	private void downloadNoteToPhoneGoogle(String authToken, LocalNoteInfo localNoteInfo, ServerSideNoteInfo serverSideNoteInfo) throws Exception
	{
		// Get URI of notes database
		final ContentResolver contentResolver = activity.getContentResolver();
		final Uri tableUri = activity.getIntent().getData();
		final Uri noteUri = ContentUris.withAppendedId(tableUri, localNoteInfo._id);

		int now = (int) (System.currentTimeMillis() / 1000);
		ContentValues values = new ContentValues();
		values.put(DBConstants.REMOTE_ID, serverSideNoteInfo.server_gdocs_id);
		values.put(DBConstants.TITLE, serverSideNoteInfo.server_title);
		values.put(DBConstants.CONTENTS, serverSideNoteInfo.server_contents);
		values.put(DBConstants.TAGS, serverSideNoteInfo.server_tags);
		values.put(DBConstants.CREATED_ON, serverSideNoteInfo.server_created_on);
		values.put(DBConstants.MODIFIED_ON, serverSideNoteInfo.server_modified_on);
		values.put(DBConstants.SYNCED_ON, Math.max(now, serverSideNoteInfo.server_modified_on));
		values.put(DBConstants.DONT_SYNC_THIS_NOTE, 0);
		values.put(DBConstants.DELETED_FROM_PHONE, 0);

		int updated = contentResolver.update(noteUri, values, null, null);
		if(updated <= 0) {
			fatalError.append("Couldn't update database" + "\n");
			throw new IOException("Couldn't update note: " + noteUri);
		}
	}

	/**
	 * Tells the SyncThread to exit.
	 */
	@Override
	public void requestStop()
	{
		Log.i(NotepadStrings.LOG_TAG, "SyncThread: STOP REQUESTED");
		stop = true;
	}



	/**
	 * Changes the message in the Sync dialog.
	 * @param mediumText
	 * @param smallText
	 */
	private void setText(final String mediumText, final String smallText)
	{
		mHandler.post(new Runnable() {

			public void run() {

				activity.setText(mediumText, smallText);
			}
		});
	}

	/**
     * Shows the button panel and sets the buttons.
     */
    protected void showButtonPanel(final String[] buttonText, final OnClickListener buttonListener)
    {
    	mHandler.post(new Runnable() {

			public void run()
			{
				activity.showButtonPanel(buttonText, buttonListener);
			}
		});
    }


    /**
     * Hides the button panel
     */
    public void hideButtonPanel()
    {
    	mHandler.post(new Runnable() {

			public void run() {

				activity.hideButtonPanel();
			}
		});
    }



	/**
	 * Queries user and doesn't return until the user chooses a result.
	 * @return the button # chosen (starting from 0)
	 */
	private int askUser( String[] buttonText ) throws IOException
	{
		ButtonListener listener = new ButtonListener(buttonText);
		showButtonPanel(buttonText, listener);

		while(listener.getSelection() == null)
		{
			//wait for user to make a choice
			try {
				Thread.sleep(30);

				if(stop)
				{
					break;
				}
			} catch(Exception e) {

			}
		}

		hideButtonPanel();

		if(stop)
		{
			fatalError.append("canceled");
			throw new IOException("Stop requested");
		}

		return listener.getSelection();
	}





	/**
	 * Different possible scenarios for resolving conflicting edits on client, server.
	 */
	private enum ConflictResolutionOptions
	{
		DOWNLOAD_TO_PHONE,
		UPLOAD_TO_SERVER,
		CREATE_ON_SERVER,
		DELETE_FROM_PHONE,
		DELETE_FROM_SERVER,
		SKIP,
		;

		@Override
		public String toString() {
			switch(this)
			{
			case DELETE_FROM_PHONE:
				return "Delete\nfrom phone";
			case DELETE_FROM_SERVER:
				return "Delete\nfrom server";
			case SKIP:
				return "Skip";
			case DOWNLOAD_TO_PHONE:
				return "Download\nto phone";
			case UPLOAD_TO_SERVER:
				return "Upload\nto server";
			case CREATE_ON_SERVER:
				return "Upload\nto server";
			default:
				throw new IllegalStateException("Unexpected enum: " + this);
			}
		}

	}



	/**
	 * Button listener.
	 */
	protected class ButtonListener implements OnClickListener
	{

		String[] buttonText;
		Integer selection = null;

		public ButtonListener(String[] buttonText)
		{
			this.buttonText = buttonText;
		}

		@Override
		public void onClick(View v)
		{
			for(int i = 0; i < buttonText.length; i++)
			{
				if( ((Button) v).getText().equals( buttonText[i] ) )
				{
					selection = i;
					break;
				}
			}
		}

		/**
		 * Returns null until a selection has been made, at which point it returns the button # that was pressed.
		 * @return
		 */
		public Integer getSelection()
		{
			return selection;
		}
	};

	/**
	 * Info about a note received from the gdocs server
	 */
	protected class ServerSideNoteInfo
	{
		public String server_gdocs_id;
		public String server_title;
		public String server_contents;
		public String server_tags;
		public int server_created_on;
		public int server_modified_on;

	}

	/**
	 * Info about a note stored on the phone.
	 */
	protected class LocalNoteInfo
	{
		public int _id;
		public String local_title;
		public String local_contents;
		public String local_tags;
		public boolean is_deleted_from_phone;
		public int dont_sync_this_note;
		public int local_modified_on;
		public int synced_on;

	}

	/**
	 * Sets a handler which will execute when the thread is about1 to finish.
	 * It will be one of the last things the thread executes.
	 *
	 * @param runnable
	 */
	public void setThreadFinishedHandler(Runnable runnable)
	{
		this.threadFinishedHandler  = runnable;
	}
}
